package com.bls.productmall.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bls.productmall.config.WxPayConfig;
import com.bls.productmall.entity.Order;
import com.bls.productmall.entity.Refund;
import com.bls.productmall.enums.OrderStatus;
import com.bls.productmall.enums.wxpay.WxApiType;
import com.bls.productmall.enums.wxpay.WxNotifyType;
import com.bls.productmall.enums.wxpay.WxRefundStatus;
import com.bls.productmall.mapper.RefundMapper;
import com.bls.productmall.service.OrderService;
import com.bls.productmall.service.RefundService;
import com.bls.productmall.util.OrderNoUtils;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> implements RefundService {

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private OrderService orderService;

    @Resource
    private CloseableHttpClient wxPayClient;

    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 根据订单号创建退款订单
     */
    @Override
    public Refund createRefundByOrderNo(String orderNo, String reason) {
        //根据订单号获取订单信息
        Order order = orderService.getOrderByOrderNo(orderNo);
        //根据订单号生成退款订单
        Refund refund = new Refund();
        refund.setOrderNo(orderNo);//订单编号
        refund.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
        refund.setTotalFee(order.getTotalFee());//原订单金额(分)
        refund.setRefund(order.getTotalFee());//退款金额(分)
        refund.setReason(reason);//退款原因
        //保存退款订单
        baseMapper.insert(refund);
        return refund;
    }

    /**
     * 记录退款记录
     */
    @Override
    public void updateRefund(String content) {
        //将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号修改退款单
        QueryWrapper<Refund> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
        //设置要修改的字段
        Refund refund = new Refund();
        //微信支付退款单号
        refund.setRefundId(resultMap.get("refund_id"));
        //查询退款和申请退款中的返回参数
        if(resultMap.get("status") != null){
            refund.setRefundStatus(resultMap.get("status"));//退款状态
            refund.setContentReturn(content);//将全部响应结果存入数据库的content字段
        }
        //退款回调中的回调参数
        if(resultMap.get("refund_status") != null){
            refund.setRefundStatus(resultMap.get("refund_status"));//退款状态
            refund.setContentNotify(content);//将全部响应结果存入数据库的content字段
        }
        //更新退款单
        baseMapper.update(refund, queryWrapper);
    }

    /**
     * 找出申请退款超过minutes分钟并且未成功的退款单
     * @param minutes
     * @return
     */
    @Override
    public List<Refund> getNoRefundOrderByDuration(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        QueryWrapper<Refund> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
        queryWrapper.le("create_time", instant);
        List<Refund> refundList = baseMapper.selectList(queryWrapper);
        return refundList;
    }

    /**
     * 退款(订单号、理由)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) throws Exception {
        log.info("创建退款单记录");
        //根据订单编号创建退款单
        Refund refundsInfo = this.createRefundByOrderNo(orderNo, reason);
        log.info("调用退款API");
        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
        Map amountMap = new HashMap();
        amountMap.put("refund", refundsInfo.getRefund());//退款金额
        amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");//退款币种
        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}" + jsonParams);
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json");//设置响应报文格式

        //完成签名并执行请求，并完成验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            //解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
            }
            //更新订单状态
            orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
            //更新退款单
            this.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

    /**
     * 查询退款接口调用
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {
        log.info("查询退款接口调用 ===> {}", refundNo);
        String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);
        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

    /**
     * 根据退款单号核实退款单状态
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkRefundStatus(String refundNo) throws Exception {
        log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
        //调用查询退款单接口
        String result = this.queryRefund(refundNo);
        //组装json请求体字符串
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端退款状态
        String status = resultMap.get("status");
        String orderNo = resultMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            log.warn("核实订单已退款成功 ===> {}", refundNo);
            //如果确认退款成功，则更新订单状态
            orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
            //更新退款单
            this.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常  ===> {}", refundNo);
            //如果确认退款成功，则更新订单状态
            orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
            //更新退款单
            this.updateRefund(result);
        }
    }

    /**
     * 处理退款单
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {
        log.info("退款单");
        //解密报文
        String plainText = decryptFromResource(bodyMap);
        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");
        if(lock.tryLock()){
            try {
                String orderStatus = orderService.getOrderStatus(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态
                orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
                //更新退款单
                this.updateRefund(plainText);
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 对称解密
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");
        log.info("密文 ===> {}", ciphertext);
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);
        log.info("明文 ===> {}", plainText);
        return plainText;
    }

}
